Skip to content

Add LFM2 and LFM2.5 support and testing#6428

Merged
qgallouedec merged 7 commits into
mainfrom
lfm2-support
Jul 23, 2026
Merged

Add LFM2 and LFM2.5 support and testing#6428
qgallouedec merged 7 commits into
mainfrom
lfm2-support

Conversation

@qgallouedec

@qgallouedec qgallouedec commented Jul 16, 2026

Copy link
Copy Markdown
Member

LFM2 sees roughly 1000 runs per week as per our telemetry, but the architecture/template isn't covered by our test suite at all. This adds both, for the two generations that behave differently:

  • trl-internal-testing/tiny-Lfm2ForCausalLM (from LiquidAI/LFM2-1.2B), landed in transformers 4.54.0, below our floor.
  • trl-internal-testing/tiny-Lfm2ForCausalLM-2.5 (from LiquidAI/LFM2.5-230M), gated on transformers>=5.0.0, which its TokenizersBackend tokenizer requires.

Both are wired into the usual parametrized lists (test_data_utils, test_utils, test_sft_trainer,
test_dpo_trainer, test_chat_template_utils).

Chat templates

The two generations differ in a way worth stating plainly:

  • LFM2 renders tools and the tool role, but never reads message["tool_calls"]: an assistant tool call renders as an empty turn. So it goes in the does-not-support tool-calling list, and gets a lfm2_training.jinja patch since its template has no {% generation %} markers.
  • LFM2.5 does render tool_calls, as <|tool_call_start|>[name(key=value, ...)]<|tool_call_end|>. It needs no training patch (its template already ships generation markers and is prefix-preserving), but it does get an lfm2_2_5_template response template so tool calls can be parsed back. Only the new-style template is provided; the legacy response_schema is on its way out and isn't worth adding for a new family.

One known limitation, inherent to the upstream template: argument values are rendered with Jinja's string filter, so flag=False parses back as the string "False". Strings, numbers, and nested JSON round-trip fine.

⚠️ Incidental finding: chunked LM head is bf16-lossy for tied embeddings

Adding LFM2 to _CHUNKED_LM_HEAD_MODEL_IDS surfaced a pre-existing issue. For models that tie their embeddings, the chunked LM head's grad_hidden reaches lm_head.weight through the embedding, where the chunked path (fp32 accumulation) and the reference (whose .float() backward casts to bf16) round differently. Both end up several percent from the true fp32 gradient.

It is not LFM2-specific, the behaviour splits cleanly on tie_word_embeddings across five architectures (~1e-6 fp32 relative error tied vs ~1e-11 untied). Gemma and Cohere are tied too, and stay inside tolerance only because their gradients are ~40x smaller.

Follow-ups?

  • LFM2.5-1.2B-Instruct ships a third template variant (308k downloads/month, floor-loadable) that currently hard-fails get_training_chat_template. Highest-value next step.
  • LFM2-VL: Support LFM2-VL multimodal inputs in GRPO and RLOO #6114 shipped tile-indexed handling in GRPO/RLOO that is only tested against synthetic tensors today.

Note

Low Risk
Changes are mostly new Jinja templates, response parsing, and test/param wiring for LFM2 families; no auth, data, or core training algorithm changes beyond template auto-selection.

Overview
Adds first-class support for Liquid LFM2 and LFM2.5 in TRL’s chat-template stack and test matrix.

LFM2 gets bundled lfm2.jinja / lfm2_training.jinja: the training patch splits assistant turns so <|im_start|>assistant\n stays outside {% generation %} while content and <|im_end|> are masked for assistant-only SFT. It is registered for automatic training-template swap but classified as not supporting standard tool_calls (tools/tool roles render; assistant tool_calls are ignored).

LFM2.5 adds lfm2_2_5.jinja plus a new lfm2_2_5_template response parser (thinking + Python-style <|tool_call_start|>[fn(a=1)]<|tool_call_end|> blocks). add_response_schema maps this family to the new-style template only (transformers ≥ 5.13) and errors on older versions without a legacy schema.

Tiny checkpoint generators (lfm2_for_causal_lm.py, lfm2_for_causal_lm_2_5.py) and parametrized tests wire both models into SFT/DPO, data utils, chat-template utilities, and chunked LM-head coverage (LFM2.5 gated on transformers ≥ 5.0).

Docs and trl/chat_templates/README.md document the two generations’ tool-calling behavior.

Separately, test_backward for chunked LM head now runs in float32 with tighter tolerances so tied-embedding bf16 noise does not mask gradient mismatches (issue surfaced when LFM2 joined the model list).

Reviewed by Cursor Bugbot for commit f7626c4. Bugbot is set up for automated code reviews on this repo. Configure here.

@bot-ci-comment

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Comment on lines +449 to +487
lfm2_2_5_template = {
# LFM2.5 renders every tool call of a turn into a single `<|tool_call_start|>[...]<|tool_call_end|>` block, as a
# comma-separated list of Python-style calls: `[get_weather(city='Paris', days=3), ping()]`. The `open_pattern`
# therefore matches either the opening bracket or the comma separating two calls, and `close_pattern` matches the
# closing paren plus, for the last call, the trailing `]<|tool_call_end|>`.
#
# Argument values are rendered by the template's `format_arg_value` macro: strings single-quoted, mappings as JSON,
# everything else via Jinja's `string` filter. `string_delims` turns the single-quoted strings back into JSON
# strings so they parse. Note that this round-trip is lossy for Python literals the `string` filter emits but JSON
# cannot express: `flag=False` parses back as the string `"False"`, not `False`. That's inherent to the upstream
# template, not something we can recover here.
#
# The thinking field is named `thinking` (not `reasoning_content` as in other families) to match what the LFM2.5
# template itself reads back off the message.
"defaults": {"role": "assistant"},
"start_anchor": "<|im_start|>assistant\n",
"fields": {
"thinking": {
"open": "<think>",
"close_pattern": r"</think>\s*",
"content": "text",
},
"tool_calls": {
"open_pattern": r"(?:<\|tool_call_start\|>\[|,\s*)(?P<name>\w+)\(",
"close_pattern": r"\)(?:\]<\|tool_call_end\|>)?\s*",
"repeats": True,
"content": "xml-inline",
"content_args": {
"tag_pattern": r"(?P<key>\w+)=(?P<value>'[^']*'|\{.*?\}|[^,)]*)",
"value_parser": {"name": "json", "args": {"string_delims": [["'", "'"]], "allow_non_json": True}},
},
"transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
},
"content": {
"close_pattern": r"<\|im_end\|>\s*",
"content": "text",
},
},
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rocketknight1 for information

@kashif kashif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious if the CI passes

Comment thread tests/test_utils.py Outdated
Comment thread trl/chat_template_utils.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e29c92649

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread trl/chat_template_utils.py Outdated
"repeats": True,
"content": "xml-inline",
"content_args": {
"tag_pattern": r"(?P<key>\w+)=(?P<value>'[^']*'|\{.*?\}|[^,)]*)",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve composite LFM2.5 tool arguments

When an LFM2.5 tool call has an array argument or a nested mapping, this regex stops the value at the first comma or first }. The companion template can render valid calls such as foo(items=[1, 2]) or foo(cfg={"a":{"b":1},"c":2}), but response parsing turns those into truncated strings like "[1" or partial JSON before the tool is executed. This silently corrupts tool arguments for GRPO/response parsing; the value parser needs to consume balanced list/object values rather than splitting on commas/braces.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, reproduced: foo(items=[1, 2]) parsed back as {"items": "[1"}, and foo(cfg={"a": {"b": 1}, "c": 2}) as truncated JSON. The value pattern had no branch for [, and its \{.*?\} branch was non-greedy. Fixed by matching balanced brackets (quote-aware, up to three nesting levels, the regex-fixed-depth cap, noted in the comment). Lists, nested mappings, and lists-of-objects now round-trip.

Not sure if it's the best solution, but it seems to work.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c4b6d94. Configure here.

Comment thread tests/test_utils.py
chunked_grad = model_chunked.lm_head.weight.grad.clone()

torch.testing.assert_close(chunked_grad, ref_grad, atol=5e-2, rtol=5e-2)
torch.testing.assert_close(chunked_grad, ref_grad, atol=1e-3, rtol=1e-3)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tied-embedding backward xfail

Medium Severity

This PR adds LFM2 models to _CHUNKED_LM_HEAD_MODEL_IDS and tightens test_backward, but the PR notes tied-embedding models should xfail at temperature != 1.0. No xfail or skip on tie_word_embeddings appears, so CI can fail for LFM2 and other tied tiny models at temperature=0.7.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c4b6d94. Configure here.

@qgallouedec
qgallouedec merged commit 85dd510 into main Jul 23, 2026
13 checks passed
@qgallouedec
qgallouedec deleted the lfm2-support branch July 23, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants